#include "ReadsFileParser.h"

CReadsSetParam::CReadsSetParam(void)
{
    this->bDiscardReadsWN = false;
    this->fileFormat[0] = '\0';
    this->uiReadLength = 0;
    // This filter setting will filter all the reads which must be reset
    this->filteredDummyBases = 0;
    this->filteredRepeatBases = 0;
}

CReadsSetParam::~CReadsSetParam(void)
{

}

inline bool isNorDot(char c)
{
    return(c == '.' || c == 'N');
}

CReadsFileParser::CReadsFileParser(void)
{
    this->initialization();
}

CReadsFileParser::~CReadsFileParser(void)
{
    ifile.close();
    delete this->pBuf;
}

int CReadsFileParser::initialization(void)
{
    this->pBuf = NULL;
    this->cFileType = 'F';
    this->caNextRead[0] = '\0';
    this->caNextReadTag[0] = '\0';
    this->caNextReadQSs[0] = '\0';
    this->InputFile[0] = '\0';
    return(0);
}

char CReadsFileParser::openAFileReady2GetRead(const char* filename, const CReadsSetParam &Param)
{
    this->opt = Param;
    this->cFileType = getReadsFileFormatSymbol(filename, Param.fileFormat);
    myStrCpy(this->InputFile, filename, FILENAME_MAX);

    this->ifile.open(InputFile);
    if (this->ifile.bad() || !fileExist(InputFile)) {
        char tmpWorkDir[MAX_PATH];
        get_working_directory(tmpWorkDir);
        LOG_INFO("\nInfo %d: Cannot open reads file %s in %s\n",\
                 WARNING_LOG, InputFile, tmpWorkDir);
        return(this->cFileType);
    } else {
        this->pBuf = new FileInputBuffer(READS_INPUT_BUFFER_SIZE, &ifile);
        if (this->pBuf == NULL) ERR; // Fail to new FileInputBuffer
    }
    return(this->cFileType);
}

/*
 * This function read in the next read from the inputfile (buffer) this->pBuf.
 * Currently it only read the sequence (no quality) in the following format.
 */
char* CReadsFileParser::get_Next_Read(void)
{
    // If there are still something to read from file or buffer
    // The kmer will be read to this->caNextRead and return it
    this->caNextRead[0] = '\0';
    if (this->pBuf->ready2Read()) {
        switch (this->cFileType) {
        case 'F':
            get_Next_Read_From_Fasta();
            break;
        case 'S':
            get_Next_Read_From_csFasta();
            break;
        case 'q':
            get_Next_Read_From_Fastq();
            break;
        case 'Q':
            get_Next_Read_From_csFastq();
            break;
        default:
            LOG_INFO("Info %d: Unknown read foramt. Parse as fasta\n", CONFIG_LOG);
            get_Next_Read_From_Fasta();
        }
    }
    if (ifile.eof() == true) {
        ifile.close();
    }
    return(this->caNextRead);
}

char* CReadsFileParser::get_Next_Read_From_Fasta(void)
{
    char caBuf[ MAX_CHAR_PER_LINE ];
    char* pch;

    while (1) { // Line which don't start with '>' or nucleotide symbol will be ignored.
        if (this->pBuf->Getline(caBuf, MAX_CHAR_PER_LINE - 1) == 0) {
            this->caNextRead[0] = '\0'; // It is file end
            break;
        } else {
            if (caBuf[0] == ' ' || caBuf[0] == '\t' || caBuf[0] == EOF) {
                this->caNextRead[0] = '\0'; // It is file end
                break;
            } else if (caBuf[0] == '>' && !ifile.eof()) {
                // fix the problem when a space after '>' by getReadIdFromTagLine()
                pch = strtok(&caBuf[1], " ,\t\n"); //This should be the name
                if (pch != NULL) {
                    sprintf(this->caNextReadTag, ">%s", pch);
                } else {
                    myStrCpy(this->caNextReadTag, ">", FILENAME_MAX);
                }
                continue;
            } else if ( !isNucleotide(caBuf[0]) && !isNorDot(caBuf[0]) && !ifile.eof()) {
                continue;
            }
            pch = strtok(caBuf, " ,\t\n"); //This should be the name
            unsigned int line_length = (unsigned int)strlen(pch);
            if (line_length >= opt.uiReadLength) {
                strncpy(this->caNextRead, pch, opt.uiReadLength);
                this->caNextRead[opt.uiReadLength] = '\0';
                break;
            } else if (line_length < opt.uiReadLength) {
                // May be the read is too long so is print in the next line
                this->pBuf->Getline(&caBuf[line_length], MAX_CHAR_PER_LINE - line_length);
                line_length = (unsigned int)strlen(caBuf);
                if (line_length != opt.uiReadLength) {
                    LOG_INFO("Info %d: Read %s has length %d, shorter than expected\r"\
                             ,WARNING_LOG,this->caNextReadTag, line_length);
                    this->caNextRead[0] = '\0';
                    continue;
                } else {
                    myStrCpy(this->caNextRead, pch, MAX_LINE);
                    break;
                }
            }
        }
    }
    return(this->caNextRead);
}

#define GET_LINE_UNTIL(pBuf, caBuf, syntex) {\
   while (1) {\
        if(pBuf->Getline(caBuf, MAX_CHAR_PER_LINE - 1) == 0) {\
            return(false);\
        }\
        syntex;\
   }\
   return(true);\
}

inline bool CReadsFileParser::getNextSeqNameInFq(FileInputBuffer* pBuf, char* caBuf)
{
    GET_LINE_UNTIL(pBuf, caBuf, {
        if ( caBuf[0] == '@' && !ifile.eof()) {
            myStrCpy(this->caNextReadTag, caBuf, FILENAME_MAX);
            return(true);
        } else if (caBuf[0] == '>' ) {
            LOG_INFO("Info %d: Invalid fastq file. Is it a fasta file?\n", WARNING_LOG);
            return(false);
        }
    })
}
inline bool CReadsFileParser::getNextLine(FileInputBuffer* pBuf, char* caBuf, const char exp1stChar)
{
    GET_LINE_UNTIL(pBuf, caBuf, {
        if ( caBuf[0] == exp1stChar && !ifile.eof()) {
            return(true);
        }
    })
}

inline bool CReadsFileParser::getNextSeqInFq
(FileInputBuffer* pBuf, char* caBuf, unsigned int expLength)
{
    GET_LINE_UNTIL(pBuf, caBuf, {
        if ((isNucleotide(caBuf[0]) || isNorDot(caBuf[0])) && !ifile.eof()) {
            unsigned int line_length = (unsigned int)strlen(caBuf);
            if ( line_length >= expLength ) {
                strncpy(this->caNextRead, caBuf, expLength);
                this->caNextRead[expLength] = '\0';
                return(true);
            } else {
                LOG_INFO("Info %d: Read %s has length %d, shorter than expected.\n",\
                         WARNING_LOG, caBuf, line_length);
                this->caNextRead[0] = '\0';
                return(false);
            }
        }
    })
}

inline bool CReadsFileParser::getNextQScoreInFq(FileInputBuffer* pBuf, char* caBuf, unsigned int expLength)
{
    GET_LINE_UNTIL(pBuf, caBuf, {
        if ((unsigned int)strlen(caBuf) >= expLength) {
            return(true);
        }
    })
}

char* CReadsFileParser::get_Next_Read_From_Fastq(void)
{
    char caBuf[ MAX_CHAR_PER_LINE ];
    if (getNextSeqNameInFq(this->pBuf, caBuf)) {
        if (getNextSeqInFq(this->pBuf, caBuf, opt.uiReadLength)) {
            if (getNextLine(this->pBuf, caBuf,'+')) {
                if (getNextQScoreInFq(this->pBuf, caBuf, opt.uiReadLength)) {
                    trQScores(this->opt.uiReadLength, -1 * SolexaScoreEncodingShift, caBuf, this->caNextReadQSs);
                    return(this->caNextRead);
                }
            }
        }
    }
    // if EOF (file end)
    caNextRead[0] = '\0';
    ifile.close();
    return(this->caNextRead);
}

char* CReadsFileParser::get_Next_Read_From_csFastq(void)
{
    char caBuf[ MAX_CHAR_PER_LINE ];
    if (getNextSeqNameInFq(this->pBuf, caBuf) &&\
            getNextSeqInFq(this->pBuf, caBuf, opt.uiReadLength + 1) &&\
            getNextLine(this->pBuf, caBuf,'+') &&\
            getNextQScoreInFq(this->pBuf, caBuf, opt.uiReadLength)) {
        trQScores(opt.uiReadLength, -1 * Phred_SCALE_QUAL_SHIFT, caBuf, this->caNextReadQSs);
        // sucessfully get a read
    } else { // if EOF (file end)
        caNextRead[0] = '\0';
        ifile.close();
    }
    return(this->caNextRead);
}

char* CReadsFileParser::get_Next_Read_From_csFasta(void)
{
    // A polished version of et_Next_Read_From_fasta

    char caBuf[ MAX_CHAR_PER_LINE ];
    while (1) {
        caBuf[0] = '\0';
        //Note this->pBuf->Getline() will return 0 if EOF is meet O
        if (this->pBuf->Getline(caBuf, MAX_CHAR_PER_LINE - 1) == 0) {
            this->caNextRead[0] = '\0'; // It is file end
            ifile.close();// Must close the file
            break;
        } else {
            //If this line is header, new line, comment or null line however not EOF, read the next line
            if (caBuf[0] == '>' && !ifile.eof() ) {
                myStrCpy(this->caNextReadTag, caBuf, FILENAME_MAX);
                continue;
            } else if ( !isNucleotide(caBuf[0]) && !isNorDot(caBuf[0]) && !ifile.eof()) {
                continue;
            } else {
                unsigned int line_length = (unsigned int)strlen(caBuf);
                // Note the first base is from the primer not chromosome
                // The read length is counted as the # of colors 0, 1, 2, 3
                if (line_length == (opt.uiReadLength + 1)) {
                    myStrCpy(this->caNextRead, caBuf, MAX_LINE);
                    break;
                } else if (line_length > (opt.uiReadLength + 1)) {
                    myStrCpy(this->caNextRead, caBuf, opt.uiReadLength + 1);
                    break;
                } else {
                    LOG_INFO("Info %d: Read %s has length %d, shorter than expected.\n",\
                             WARNING_LOG, caBuf, line_length);
                    this->caNextRead[0] = '\0';
                    continue;
                }
            }
        }
    }
    return(this->caNextRead);
}

unsigned int estimateNoOfReads(const char* fileName, const char* fileFormat)
{
    /* TEST TIME
    cout << "Calculate the get line time " << endl;
    TIME_INFORMATION(estimateNoOfReads_From_Fasta(fileName));
    cout << "Caculate the number of > " << endl;
    TIME_INFORMATION(estimateNoOfReads_From_SOLID(fileName));
    cout << "Which is faster " << endl; */
    char fileType = getReadsFileFormatSymbol(fileName, fileFormat);
    switch (fileType) {
    case 'F':
    case 'S':
        return(estimateNoOfReads_From_Fasta(fileName));
    case 'Q':
    case 'q':
        return(estimateNoOfReads_From_Fastq(fileName));
    default:
        break;
    }
    return (MAX_READ_SET_CAPACITY);
}

unsigned int estimateNoOfReads_From_Fasta(const char* fileName)
{
    // Assume each line has a header
    unsigned int uiNoOfRead = (unsigned int)getNumberOfLineInAFile(fileName) / 2;
    return(uiNoOfRead);
}

unsigned int estimateNoOfReads_From_Fastq(const char* fileName)
{
    // Assume each line has a header
    if (fileExist(fileName)) {
        unsigned int uiNoOfRead = (unsigned int)getNumberOfLineInAFile(fileName) / 4;
        return(uiNoOfRead);
    } else {
        return(0);
    }
}

// check if the format string is for fastq
inline bool isFqFormatStr(const char* fileFormat)
{
    bool bFqFormatStr = (strncmp(fileFormat, "fq", 2) == 0) || \
                        (strncmp(fileFormat, ".fq", 3) == 0) || \
                        (strncmp(fileFormat, "fastq", 5) == 0) ||\
                        (strncmp(fileFormat, ".fastq", 6) == 0);
    return(bFqFormatStr);
}

// Return true fo csfastq or csfq. Judge by content for fastq.
bool is_csFastq_format(const char* fileName, const char* fileFormat)
{
    if (hasCsfqExtName(fileName) || hasCsfqExtName(fileFormat)) {
        return(true);
    }
    bool returnValue = false;
    if (hasFqExtName(fileName)) {
        // A simple check if it is a fastq format by
        if (isFqFormatStr(fileName)) {
            ; // this is a format string setting. It should be fastq format.
        } else if ( fileExist(fileName) ) {
            ifstream ifile(fileName);
            char caBuf[MAX_LINE];
            do {
                caBuf[0] = '\0';
                ifile.getline(caBuf, MAX_LINE);
                if (caBuf[0] == '@') {
                    ifile.getline(caBuf, MAX_LINE);
                    returnValue = isACGT(caBuf[0]) && is0123(caBuf[1]);
                    break;
                }
            } while (!ifile.eof());
            ifile.close();
        } else {
            cout << fileName << " doesn't exist." << endl;
            cout << "Can't decide the format based on the ext name." << endl;
        }
    }
    return (returnValue);
}

char getReadsFileFormatSymbol(const char* InputFile, const char* fileFormat)
{
    string formatStr(getExtName(fileFormat));
    if (formatStr[0] != '.') {
        formatStr = string(".").append(formatStr);
    }
    char formatSymbol = getReadsFileFormatSymbol(formatStr.c_str());
    if (formatSymbol == 'N') {
        formatSymbol = getReadsFileFormatSymbol(InputFile);
        if (formatSymbol == 'N') {
            LOG_INFO("Info %d: Unknown reads format.\n", WARNING_LOG);
        }
    }
    return(formatSymbol);
}

char getReadsFileFormatSymbol(const char* fileName)
{
    if (hasTheExtName(fileName, ".fasta") ||
            hasTheExtName(fileName, ".fa") ||
            hasTheExtName(fileName, ".mfa") ||
            hasTheExtName(fileName, ".fna")) {
        return('F');
    } else if (hasTheExtName(fileName, ".csfasta") ||
               hasTheExtName(fileName, ".csfa")) {
        return('S');
    } else if (is_csFastq_format(fileName)) {
        // Note csFastq format can have a ext name .fastq or .fq
        return('Q');
    } else if (hasTheExtName(fileName, ".fastqsanger") ||
               hasTheExtName(fileName, ".fastq") ||
               hasTheExtName(fileName, ".fq")) {
        return('q');
    } else {
        return('N');
    }
}

bool is_colorspace_reads(const char* fileName)
{
    char fileFormat = getReadsFileFormatSymbol(fileName);
    return(fileFormat == 'S' || fileFormat == 'Q');
}

void getReadsFileFormat(const char* fileName, char* fileFormat)
{

    const char formatSymbol = getReadsFileFormatSymbol(fileName, fileFormat);
    switch (formatSymbol) {

    case 'F':
        strcpy(fileFormat, "fasta");
        break;
    case 'q':
        strcpy(fileFormat, "fastq");
        break;
    case 'Q':
        strcpy(fileFormat, "csfastq");
        break;
    case 'S':
        strcpy(fileFormat, "csfasta");
        break;
    default:
        LOG_INFO("Info %d: Unknown read file format.\n", WARNING_LOG);
    }
}

void getReadIdFromTagLine(char* readIdStr, const char* tagLine, unsigned int readIdNo, char sep)
{
    int trimStart;
    for (trimStart = 1; tagLine[trimStart]!= '\0'; trimStart++) {
        if (!(isspace(tagLine[trimStart]) || tagLine[trimStart] == sep)) {
            break;
        }
    }
    if ( tagLine[trimStart] == '\0') {
        makeFakeReadId(readIdStr, readIdNo);
    } else {
        strncpy(readIdStr, &tagLine[trimStart], READ_ID_LENGTH - 1);
        readIdStr[READ_ID_LENGTH - 1] = '\0';
        formatReadId(readIdStr, sep);
    }
}
